Now
we're ready to upgrade NavApp for the iPad. Before kicking off this
process, make a backup copy of your project directory. This can be
useful in case you want to compare your original app with the
iPad-ready version that Xcode sets up.
Next, in the Groups & Files section of your Xcode project window, open the Targets section and select NavApp. Then select Project Upgrade Current Target for iPad
from the menu. Xcode will present you with a modal sheet that asks
whether you want to create a single universal application that will run
on both iPhone and iPad, or create a second target for an iPad
application in your project, leaving the original target intact. Select
the One Universal application option, and then click OK.
Xcode now does a few simple things. It copies the MainWindow.xib file to MainWindow-iPad.xib, making a few changes to the file's contents, such as specifying the iPad's screen size. The new .xib file is added to the project, and a line is added to the NavAppCompare-Info.plist file, specifying that this new .xib
file should be used when launching on the iPad. It also makes a few
changes to your project, such as setting the base SDK to 3.2.
1. Taking the Upgraded NavApp for a Spin
Make sure that Simulator
3.2 is chosen as the Active SDK in Xcode, and then build and run the
upgraded app. You should see NavApp spring to life, full size and at
full resolution, in the Simulator. It will work just as it did in
iPhone form, letting you drill down through the structure we laid out
previously and displaying a similar result, as shown in Figure 1.
Although this type of
upgrade works, it isn't what you really want for an iPad app. The
popular iPhone pattern of drilling down into structures, with the
entire content of the screen sliding out the side, isn't prevalent on
the iPad. In fact, Apple actually recommends against that usage, for a
couple of reasons:
The full-screen
wipe in response to simple tap on the screen, which works well enough
on a small display such as the iPhone's, begins to feel a little off on
a larger screen. The full-screen swish is best reserved for situations
where the user actually made a swiping gesture.
Since
the iPad has so much more screen real estate, you can easily show a
drill-down navigation view alongside of, or hovering in front of, the
main content, by using a split view or a popover view (as you've seen
in earlier chapters).
So, let's rethink the NavApp GUI for the iPad version.
Reconsidering iPhone Design
ChoicesWhile the navigation views are the first things you see in
NavApp, they're actually just stepping-stones leading to the app's main
content view, which is handled by the ChoiceViewController class.
For the iPad version of
this app, let's rework the design so that the final view is now front
and center. We'll go about this by reconfiguring some .xib
files, conditionally changing the behavior of the navigation views in
response to user actions, and extending the ChoiceViewController
class so that it can display something reasonable, even when the user
hasn't selected anything yet. The navigation views will end up being
displayed in the left-hand side of a split view, or in a floating
popover view, depending on whether the iPad is in landscape or portrait
mode. This is similar to what we did in the Dudel application earlier
in this book, and even goes a step closer to the way that the iPad's
built-in Mail application handles drilling down through accounts and
folders to reach your messages.
The first step toward making this work will be to redefine what the NavAppAppDelegate class does, both in code and in its related .xib
files. This class was created automatically when we created the Xcode
project, and in its original form, it sets up the navigation interface
(since that's the kind of project we created). We're going to add a bit
of code that checks at runtime to see if we're running on an iPad, and
if so, instead set up a split view interface. The other half of this
redesign will be configuring the MainWindow-iPad.xib file so that it actually wraps things up in a split view.
2. Conditional Behavior: Know Your Idioms
Open NavAppAppDelegate.h, and add an outlet for a future UISplitViewController as shown here:
@interface NavAppAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
UINavigationController *navigationController;
UISplitViewController *splitViewController;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;
@property (nonatomic, retain) IBOutlet UISplitViewController *splitViewController;
@end
NOTE
We're working with a class template that was generated by Xcode and contains its IBOutlet
markers in the property declarations. Rather than modifying the
generated source code to make a change that has no quantifiable effect,
we're just following the example of the surrounding code here. When in
Rome ...
Now switch over to NavAppAppDelegate.m, and configure the basics for the new outlet we created by adding this line near the top of the @implementation section:
@synthesize splitViewController;
Don't forget to free up that new resource as well:
- (void)dealloc {
[splitViewController release];
[navigationController release];
[window release];
[super dealloc];
}
Next is the interesting part of this class:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[window addSubview:[splitViewController view]];
} else {
[window addSubview:[navigationController view]];
}
[window makeKeyAndVisible];
return YES;
}
This method uses the UI_USER_INTERFACE_IDIOM
function to determine whether the app is running on an iPad. If it is,
we'll present a different view than what we show for the iPhone.
This is a pretty subtle shift. Keep in mind that the app delegate class is loaded from the app's main .xib
file, and once it's loaded and the application has finished launching,
this is the method that actually gives the application a view to
display. With this small change, we radically alter the entire
appearance and flow of the app! Of course, to make that really happen,
we'll need to make sure that our new splitViewController outlet is actually pointing at something.
3. Configuring the Main iPad GUI
It's time to reorganize the main GUI, putting the navigation view inside a split view. Open MainWindow-iPad.xib in Interface Builder, and switch the main window to column view, revealing something like Figure 2.
Here, we've
drilled down into the Navigation Controller and Root View Controller
objects, revealing the complete structure of the objects in this .xib
file. If you select the Nav App App Delegate object in this window, and
then open the connections inspector, you'll see that it has outlets
connected to the window and to the navigation controller—the two
objects that are tied together in code when the app launches—by virtue
of adding the view controller's view to the window. We're going to add
a UISplitViewController to this .xib file, and configure it so that the preceding conditional code adds the split view to the window.
Start by finding a UISplitViewController in the Library, and dragging it to the leftmost column of the .xib window. Then control-drag from the app delegate to the new split view controller, and hook up the splitViewController outlet.
The split view is meant to
display views for two view controllers at once, and by default, the one
we created will have a navigation controller and a generic view
controller. The navigation controller contains a generic table view
controller. We need to modify these objects, making them instances of
the real classes we're using in our app.
Drill down into the
Navigation Controller object inside the Split View Controller object,
and select the Table View Controller object it contains. Open the
identity inspector, and change that controller's class to RootViewController. Then switch to the attribute inspector to configure the controller a little more. Set the Title to Changes, and the NIB Name to RootViewController.
Now backtrack a bit,
and select the generic View Controller object inside the Split View
Controller object. Once again, bring up the identity inspector, and set
this controller's class to ChoiceViewController
. Then switch back to the attribute inspector, and set the NIB Name to ChoiceViewController-iPad. That's the name of an .xib file that doesn't exist yet, but soon will, since we'll create it in the next section. At this point, your .xib window should look something like Figure 3.
Proper use of a split view requires you to provide the split view
controller with a delegate, which plays a role in juggling between a
split view and a popover when the iPad is rotated. In this case, we'll
connect the split view controller's delegate outlet to the choice view
controller, and later we'll implement the delegate code there. Select
the Split View Controller item so that you can see its children,
control-drag from the Split View Controller object to the Choice View
Controller object, and then connect the delegate outlet.
The final change for the MainWindow-iPad.xib file is to delete the navigation controller that's at the top level of the .xib
file. The app delegate still has an outlet to it, but that doesn't
really matter. When our app runs on an iPad, the iPad-specific .xib file is loaded, and the unconnected outlet is ignored.
4. Creating the Choice View Controller GUI for iPad
Earlier, we configured the ChoiceViewController instance in our main iPad GUI to use a special iPad-friendly .xib
file. Let's create that now. Switch back to Xcode, and use the New File
Assistant to create a new View XIB file, located in the iPhone OS /
User Interface section. Make sure the product menu is displaying iPad,
and click Next. Then name it ChoiceViewController-iPad.xib and click Next. You'll see the new file added to your project.
Before editing the new GUI, open ChoiceViewController.h and add the following instance variable:
IBOutlet UIToolbar *toolbar;
This new toolbar outlet will point at a toolbar in the iPad version of the GUI, which we'll create soon.
Open both ChoiceViewController.xib and ChoiceViewController-iPad.xib in Interface Builder. Unlike the original .xib
file, which was created along with the class, the new one is kind of a
blank slate. Select the File's Owner icon, and use the identity
inspector to set its class to ChoiceViewController. Now switch to ChoiceViewController.xib, open its view, select all the GUI objects in there, and press C to copy them. Then switch back to the new ChoiceViewController-iPad.xib
file, open its view (which you'll see is iPad-size), and paste in the
GUI objects. You'll want to center them in the display, and should
probably resize them to fill the width of the display as well—no sense
letting all that screen real estate go to waste!
Now use the Library to find a UIToolbar, and drag it to the new iPad-ready ChoiceViewController
view, dropping it at the top of the view so that the toolbar appears up
there. The toolbar contains a single default item, which you should go
ahead and delete. Finally, connect the outlets from the File's Owner
icon to the GUI objects in the .xib file: choiceLabel to the big label in the middle, toolbar to the toolbar you just created, and view (which we didn't define in out class, but inherited from UIViewController) to the entire containing view.
5. Implementing the Split View Delegate Methods
Go back to Xcode, and open the ChoiceViewController.m file. Add the two required methods for the UISplitViewController:
- (void)splitViewController:(UISplitViewController*)svc
willHideViewController:(UIViewController *)aViewController
withBarButtonItem:(UIBarButtonItem*)barButtonItem
forPopoverController:(UIPopoverController*)pc {
// add the new button item to our toolbar
NSArray *newItems = [toolbar.items arrayByAddingObject:barButtonItem];
[toolbar setItems:newItems animated:YES];
// configure the button
barButtonItem.title = @"Choices";
}
- (void)splitViewController:(UISplitViewController*)svc
willShowViewController:(UIViewController *)aViewController
invalidatingBarButtonItem:(UIBarButtonItem *)button {
// remove the button
NSMutableArray *newItems = [[toolbar.items mutableCopy] autorelease];
if ([newItems containsObject:button]) {
[newItems removeObject:button];
[toolbar setItems:newItems animated:YES];
}
}
That's all we need to
do in order to handle switching between portrait and landscape
orientation. The split view controller will call the first method when
switching to portrait mode, and the second method when switching to
landscape mode.
At this point, you should be
able to run the app. You'll see that it works ... to some extent. The
split view kicks in, displaying itself on the left side in landscape
mode, and shrinking down to a button in the toolbar in portrait mode.
Rotating from one to another isn't working yet, but we'll get to that a
little later.
The problem is in the
interaction between the view controllers themselves. All the action—not
only the navigation, but also the display of the final selection—is
constrained to the navigation view, whether it's appearing in the split
view or in a popover view. The big view for displaying the choice just
displays the default "dummy" text all the time! Clearly, we need to
update our table view controllers so that they do different things in
response to the user selecting a row, depending on whether the app is
running on an iPhone or iPad.
6. Tweaking the Navigation Logic
First, we need to make a pair of identical changes for both RootViewController.m and ChoiceViewController.m, to ensure that the views can rotate properly. In each of those files, uncomment the shouldAutorotateToInterfaceOrientation: method, and make it always return YES:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)o{
// Return YES for supported orientations.
return YES;
}
Now switch over to SecondLevelViewController.m, where we'll make some rather more critical changes. Start by adding this somewhere near the top of the file:
- #import "NavAppAppDelegate.h"
Next, uncomment the shouldAutorotateToInterfaceOrientation: method, and make it always return YES, as we just did for the RootViewController and ChoiceViewController classes.
Then, in the tableView:cellForRowAtIndexPath:
method, add a bit of code so that we don't show the final disclosure
indicator (the little right-pointing arrow/chevron that lets the users
know that they can keep on digging):
cell.textLabel.text = [NSString stringWithFormat:@"Sub-Item #%d", indexPath.row];
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
Then change the
behavior of the final selection here, so that instead of creating and
pushing another view controller onto the navigation stack, we grab the
"global" ChoiceViewController and just tell it what the selection is:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
NavAppAppDelegate *appDelegate =
[[UIApplication sharedApplication] delegate];
UISplitViewController *splitViewController =
appDelegate.splitViewController;
ChoiceViewController *detailViewController =
[splitViewController.viewControllers objectAtIndex:1];
detailViewController.choice = [NSString stringWithFormat:
@"%@, Sub-Item #%d", self.choice, indexPath.row];
} else {
ChoiceViewController *detailViewController = [[ChoiceViewController
alloc] initWithNibName:@"ChoiceViewController" bundle:nil];
detailViewController.choice = [NSString stringWithFormat:
@"%@, Sub-Item #%d", self.choice, indexPath.row];
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController
animated:YES];
[detailViewController release];
}
}
Now you should be able to
build and run the app, and see something closer to what we're shooting
for. You can pick an item and a subitem, and your choice is displayed
in the main view (and not inside the navigation view). However, this is
still a bit off. That main view is just showing default dummy values
(whatever you entered in Interface Builder) until the user selects
something, and that's not what we want.
Let's enhance ChoiceViewController a bit, so that we can display something special for the no-selection state, before the user has navigated anywhere.
7. Enhancing the Main View with a No-Selection State
Basically, the new
no-selection state will consist of hiding the labels at the top and
bottom, and putting a special text in the large center label.
Start by adding two new outlets to the class definition ChoiceViewController.h so that we can access the top and bottom labels:
IBOutlet UILabel *topLabel;
IBOutlet UILabel *bottomLabel;
Now open ChoiceViewController-iPad.xib,
and connect each of the new outlets by control-dragging from File's
Owner to each of the labels and selecting the proper outlet. Save your
changes, and go back to Xcode.
NOTE
If you're worried about
the fact that the new outlets won't be used in the non-iPad version of
the GUI, don't be! When this code runs on an iPhone and the iPhone
version of the GUI is loaded, those unconnected outlets will simply be
left as pointers to nil—no harm done.
Open ChoiceViewController.m to make a few quick changes. The first changes will be for the viewWillAppear: method, to make it display the appropriate content depending on whether or not the choice property is populated:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.choice) {
self.navigationItem.title = self.choice;
choiceLabel.text = choice;
topLabel.hidden = NO;
bottomLabel.hidden = NO;
} else {
choiceLabel.text = @"Make your choice!";
topLabel.hidden = YES;
bottomLabel.hidden = YES;
}
}
Next, we're going to implement the setChoice: method. So far, we've relied on the @synthesized version of this, but now that we need to update the display once the value is set, we should actually do something here.
- (void)setChoice:(NSString *)c {
if (![c isEqual:choice]) {
[choice release];
choice = [c copy];
self.navigationItem.title = self.choice;
choiceLabel.text = choice;
topLabel.hidden = NO;
bottomLabel.hidden = NO;
}
}
Note that we don't need to do anything here to handle the case where the new value for choice is nil
(which would theoretically require us to once again put "Make your
choice!" in the main label and hide the other labels), since in
practice, this will never occur. The only time that choice is set is when the user has just selected something, and in this app, that "something" is never nil.
At this point, you should be
able to run the app and see it working the way that we intended and
that makes the most sense, without any surprises for the users. When
you first launch the app, nothing is selected in the navigation view,
and the main display reflects this. Once you select something, your
selection sticks around in the main view until you navigate to
something else. This is pretty much identical to the behavior of other
iPad apps such as Mail, so users should feel right at home with another
app that works this way.
Thanks to the way we've
written the app, it should also continue to work on iPhone just as it
used to. To launch your app on the Simulator in iPhone mode, the key is
to build the app using the 3.2 target, then switch to the 3.1.3 (or
other iPhone OS) target, and select Run Run from the menu.